<?php

namespace Fluency\Web;

use Fluency\Web\Http\Request;
use Fluency\Web\Http\ActiveRequest;
use Fluency\Utils\AnnotationParser;

/**
 * An immutable class that dispatches requests by executing the requested actions.
 *
 * @package Fluency\Web
 * @author  Ignas Rudaitis
 * @since   1.0
 */
class RequestDispatcher
{
    private $actionResolver;
    private $filterResolver;
    private $globalFilters;
    private $renderingFilter;
    private $actions = array();
    private $annotationParsers;

    /**
     * The constructor. It takes the configuration in the form of a RequestDispatcherBuilder.
     *
     * For a cleaner syntax, using RequestDispatcher::getBuilder(), chaining all the needed setter
     * calls and calling build() at the end is preferred.
     *
     * @see getBuilder()
     *
     * @param Fluency\Web\RequestDispatcherBuilder $builder
     */
    public function __construct(RequestDispatcherBuilder $builder)
    {
        $this->actionResolver = $builder->getActionResolver();
        $this->filterResolver = $builder->getFilterResolver();
        $this->globalFilters = $builder->getGlobalFilters();
        $this->renderingFilter = new RenderingFilter($builder->getViewResolver());
        $this->annotationParsers = new \SplObjectStorage;
    }

    /**
     * Dispatches the given request and returns the response to it.
     *
     * @throws InvalidArgumentException if it is impossible to dispatch the request
     * @param Fluency\Web\Http\Request $req
     * @return Fluency\Web\Http\Response
     */
    public function dispatch(Request $req)
    {
        if (!$this->canDispatch($req)) {
            throw new \InvalidArgumentException(
                    "This RequestDispatcher cannot dispatch the specified request " .
                    "for action '{$req->getAction()}'");
        }
        $action = $this->getActionByName($req->getAction());
        $ap = $this->getAnnotationParser($action);
        $action = $this->decorateActionWithFilters($action);
        $req = new ActiveRequest($req, new RespondingPolicy($ap->getAllAnnotations()));
        $response = $action($req);
        return $response;
    }

    /**
     * Checks if the specified request can be dispatched. It cannot be dispatcher when it is
     * associated with another instance of RequestDispatcher, when its action name cannot be
     * resolved to any action and when it has external scope, but the action requested is
     * restricted (has a @Restricted annotation).
     *
     * @param Fluency\Web\Http\Request $req
     * @return boolean
     */
    public function canDispatch(Request $req)
    {
        if ($req->getDispatcher() !== $this) {
            return false;
        }
        try {
            $action = $this->getActionByName($req->getAction());
        } catch (\InvalidArgumentException $e) {
            return false;
        }
        $ap = $this->getAnnotationParser($action);
        return !($ap->hasAnnotation("Restricted") && $req->isExternal());
    }

    private function decorateActionWithFilters($action)
    {
        $ap = $this->getAnnotationParser($action);
        $names = array_merge($this->globalFilters, $ap->getAnnotations("Filter"));
        $filters = array_map($this->filterResolver, $names);
        $filters[] = $this->renderingFilter;
        return new FilterChain($filters, $action);
    }

    private function getActionByName($name)
    {
        if (isset($this->actions[$name])) {
            return $this->actions[$name];
        }
        $actionResolver = $this->actionResolver;
        $result = $actionResolver($name);
        if (!$result instanceof \Closure) {
            throw new \LogicException("Action resolver did not return a closure for name '$name'");
        }
        $this->actions[$name] = $result;
        return $result;
    }

    private function getAnnotationParser($action)
    {
        if (!isset($this->annotationParsers[$action])) {
            $reflector = new \ReflectionFunction($action);
            $doc = $reflector->getDocComment();
            $this->annotationParsers[$action] = new AnnotationParser($doc);
        }
        return $this->annotationParsers[$action];
    }

    /**
     * Returns the instance of RenderingFilter which is used by the dispatcher to render the
     * results of executed actions.
     *
     * @return Fluency\Web\RenderingFilter
     */
    public function getRenderingFilter()
    {
        return $this->renderingFilter;
    }

    /**
     * Returns a new Request object associated with the dispatcher.
     *
     * @return Fluency\Web\Http\Request
     */
    public function createRequest()
    {
        return new Request($this);
    }

    /**
     * Returns a new instance of RequestDispatcherBuilder, which can be used to build a new
     * instance of this class. The presence of this method allows for bypassing direct
     * RequestDispatcher and RequestDispatcherBuilder constructor calls and therefore makes
     * it possible to build a new RequestDispatcher instance using method call chaining, e.g.:
     * <pre>
     *   RequestDispatcher::getBuilder()
     *       ->setActionResolver(...)
     *       ->setFilterResolver(...)
     *       ->setViewResolver(...)
     *       ->build();
     * </pre>
     *
     * @return Fluency\Web\RequestDispatcherBuilder
     */
    public static function getBuilder()
    {
        return new RequestDispatcherBuilder;
    }
}
